# RobotDefense main.py

import pyglet
from pyglet.gl import *
import resource
from vec2d import Vec2d
from util import Rectangle

from fastsprite import Sprite, SpriteFrame
#from pyglet.sprite import Sprite

from gamemap import GameMap
from towers import Tower, CannonTower, GlueTower, LightningTower, VirusTower

scrap_pickup_sound = pyglet.resource.media('sounds/scrap_pickup.wav', streaming=False)
tower_remove_sound = pyglet.resource.media('sounds/remove.wav', streaming=False)

######################################################################

class MainWindow(pyglet.window.Window):
    def __init__(self):
        super(MainWindow, self).__init__(width=960, height=640,resizable=True, visible=False, fullscreen=False)
        self.set_caption('RobotDefense')
        
        # Set up the key handler
        self.keys = pyglet.window.key.KeyStateHandler()
        self.push_handlers(self.keys)

        # Call the update method every 60th of a second.
        pyglet.clock.schedule_interval(self.update, 1/60.0)
        
        # Setup OpenGL settings.
        glDisable(GL_DEPTH_TEST)
        glEnable(GL_CULL_FACE)
        glDisable(GL_DITHER)
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glClearColor(0,0,0,1)

        # Create the game controller object and set it as the active controller.
        self.game_controller = GameController(self)
        self.game_controller.on_resize(self.width, self.height)
        self.push_handlers(self.game_controller)
        self.active_controller = self.game_controller

    def on_draw(self):
        self.active_controller.draw()
        
    def update(self, dt):
        self.active_controller.update(dt)

######################################################################

class Button(object):
    """Represents a clickable button."""

    button_click_sound = pyglet.resource.media('sounds/button_click.wav', streaming=False)

    def __init__(self, name, off_image, on_image, x, y):
        self.name = name
        self.off_image = off_image
        self.on_image = on_image
        self.image = self.off_image
        self.is_pressed = False

        # action is a function called when button is pressed, with button state as sole argument.
        self.action = None
        self.hotkey = None

        self.off_image.x = x
        self.off_image.y = y

        self.on_image.x = x
        self.on_image.y = y

    def draw(self):
        self.image.draw()

    def toggle(self, is_pressed=None):
        """Reverse button state or can pass in True/False to set state to desired value."""
        if is_pressed != None:
            if self.is_pressed == is_pressed:
                # Button is already in specified state, do nothing.
                return
            self.is_pressed = is_pressed
        else:
            self.is_pressed = not self.is_pressed

        if self.is_pressed:
            self.image = self.on_image
        else:
            self.image = self.off_image
        
        self.button_click_sound.play()

        if self.action:
            self.action(self.is_pressed)

    def hit(self, x, y):
        """Returns true if point (x,y) is inside the button image."""
        return self.image.hit(x,y)

######################################################################

class SideMenu(object):
    """The SideMenu consists of a bunch of buttons shown off to the side of
    the main game window.  Each button is associated with some action that is
    activated when the button is clicked with the mouse or when its hotkey is
    pressed.  The SideMenu is also in charge of showing other game text info."""

    def __init__(self, controller):
        self.controller = controller
        self.buttons = []
        self.selected_button = None

        # Create the New Cannon Tower button
        off = Sprite(resource.texture('graphics/buttons/cannon_button_off.png', 40,40))
        on = Sprite(resource.texture('graphics/buttons/cannon_button_on.png',73,72.5))
        b = Button('Cannon Tower', off, on, 60, 500)
        b.action = self.controller.create_tower_action(CannonTower)
        b.hotkey = pyglet.window.key.C
        self.buttons.append(b)

        # Create the New Glue Tower button
        off = Sprite(resource.texture('graphics/buttons/glue_button_off.png', 40,40))
        on = Sprite(resource.texture('graphics/buttons/glue_button_on.png',72.5,71))
        b = Button('Glue Tower', off, on, 160, 500)
        b.action = self.controller.create_tower_action(GlueTower)
        b.hotkey = pyglet.window.key.G
        self.buttons.append(b)

        # Create the New Buzzsaw Tower button
        off = Sprite(resource.texture('graphics/buttons/buzzsaw_button_off.png', 40,40))
        on = Sprite(resource.texture('graphics/buttons/buzzsaw_button_on.png',72,73.5))
        b = Button('Buzzsaw Tower', off, on, 260, 500)
        b.action = self.controller.create_tower_action(LightningTower)
        b.hotkey = pyglet.window.key.B
        self.buttons.append(b)
        
        # Create the New Virus Tower button
        off = Sprite(resource.texture('graphics/buttons/virus_button_off.png', 40,40))
        on = Sprite(resource.texture('graphics/buttons/virus_button_on.png',73.5,73))
        b = Button('Virus Tower', off, on, 60, 400)
        b.action = self.controller.create_tower_action(VirusTower)
        b.hotkey = pyglet.window.key.V
        self.buttons.append(b)

        # Create the Delete button
        off = Sprite(resource.texture('graphics/buttons/delete_button_off.png', 65,40))
        on = Sprite(resource.texture('graphics/buttons/delete_button_on.png',112.5,84.5))
        b = Button('Delete', off, on, 100, 100)
        b.action = self.controller.delete_action
        b.hotkey = pyglet.window.key.D
        self.buttons.append(b)        

        self.width = 320
    
    def draw(self):
        for b in self.buttons:
            b.draw()

        scrap_label = pyglet.text.Label('Scrap: $%d' % self.controller.gp, anchor_x='left', anchor_y='top', font_size=20, bold=True, x=10, y=630)
        scrap_label.draw()

    def button_named(self, name):
        """Return sidemenu button with the give name."""
        for b in self.buttons:
            if b.name == name:
                return b
        return None
            
    def clear_buttons(self):
        """Returns all menu buttons to their default state."""
        for b in self.buttons:
            b.toggle(False)

    def toggle_button(self, button):
        """Handles radio button logic to ensure that no more than 
        one button is active at any time."""
        # Turn of the currently selected button
        if self.selected_button and self.selected_button != button:
            self.selected_button.toggle(False)
        # Toggle state of this button
        button.toggle()
        # Then if it was toggled to on state, then set is as new selected button.
        if button.is_pressed:
            self.selected_button = button
        else:
            self.selected_button = None

    def on_key_press(self, symbol, modifiers):
        """Search for a button with same hotkey as keypress and then toggle it on."""
        for b in self.buttons:
            if b.hotkey == symbol:
                self.toggle_button(b)
                return pyglet.event.EVENT_HANDLED

    def on_mouse_press(self, x, y, button, modifiers):
        """Determine if any of the buttons were pressed."""
        for b in self.buttons:
            if b.hit(x, y):
                self.toggle_button(b)
                return pyglet.event.EVENT_HANDLED

######################################################################

class GameController(object):
    def __init__(self, window):
        self.window = window
        self.map = GameMap(window)

        self.fps_display = pyglet.clock.ClockDisplay(color=(1,1,1,1))
        self.debug_draw = False
        self.debug_fps = False

        self.magnet_cursor = Sprite(resource.texture('graphics/magnet_cursor1.png', 19, 10))
        self.magnet_cursor_anim = Sprite((SpriteFrame(resource.texture('graphics/magnet_cursor2.png', 23, 11), 0.2), False))

        self.delete_cursor_active = Sprite(resource.texture('graphics/bulldozer_cursor.png', 54, 10))
        self.delete_cursor_inactive = Sprite(resource.texture('graphics/bulldozer_cursor_gray.png', 54, 10))
        self.delete_anim = Sprite((SpriteFrame(resource.texture('graphics/explode.png', 38, 30), 0.1), False))

        # Keeps track of mode which affects mouse cursor appearance and what
        # happens when the mouse button is pressed.  Some possible values are
        # 'delete', 'new tower', and None.
        self.current_mode = None
        
        self.special_cursor = None

        self.gp = 1000

        # Throw non-looping sprite animations into this list that will be shown
        # only briefly then disappear.  to be used for things like explosions.
        self.temp_sprites = []

        # When placing a new tower on map, this is set to an instance
        # of the new tower so that it can be drawn as an icon.
        self.new_tower = None

        self.sidemenu = SideMenu(self)

        glClear(GL_COLOR_BUFFER_BIT)

    def on_resize(self, width, height):
        window_aspect = float(width)/float(height)
        game_aspect = float(self.map.width + self.sidemenu.width)/float(self.map.height)

        if window_aspect < game_aspect:
            # window is taller than it needs to be, so width is the limiting dimension
            div = int(width * float(self.map.width) / float(self.map.width + self.sidemenu.width))
            h = int(width / game_aspect)
            self.map.viewport = Rectangle(0, (height - h)//2, div, h)
            self.sidemenu.viewport = Rectangle(div, (height - h)//2,  width - div, h)
        else:
            # window is longer than it needs to be so height is limiting dimension
            w = int(height * game_aspect)
            div = int(w * float(self.map.width) / float(self.map.width + self.sidemenu.width))            
            self.map.viewport = Rectangle((width - w)//2, 0, div, height)
            self.sidemenu.viewport = Rectangle((width-w)//2 + div, 0, w - div, height)
            
        return pyglet.event.EVENT_HANDLED

    def set_map_projection(self):
        glViewport(self.map.viewport.x, self.map.viewport.y, self.map.viewport.width, self.map.viewport.height)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glOrtho(0, self.map.width, 0, self.map.height, -1, 1)
        glMatrixMode(GL_MODELVIEW)

    def set_menu_projection(self):
        v = self.sidemenu.viewport
        glViewport(v.x, v.y, v.width, v.height)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glOrtho(0, self.sidemenu.width, 0, self.map.height, -1, 1)
        glMatrixMode(GL_MODELVIEW)

    def update(self, dt):
        self.map.update(dt)

        for item in self.temp_sprites[:]:
            if not item.update(dt):
                self.temp_sprites.remove(item)

    def draw(self):
        self.set_map_projection()
        glLoadIdentity()
        
        self.map.draw(self.debug_draw)

        for item in self.temp_sprites:
            item.draw()

        if self.special_cursor:
            self.special_cursor.draw()

        if self.current_mode == 'new tower' and self.new_tower.moved_yet:
            self.new_tower.draw_area_of_effect(not self.new_tower.invalid_position)
            glColor4f(1,1,1,0.5)
            self.new_tower.draw()
            glColor3f(1,1,1)

        self.set_menu_projection()
        self.sidemenu.draw()

        if self.debug_fps:
            glLoadIdentity()
            self.fps_display.draw()

    def window_to_map_coordinates(self, x, y):
        map_x = float(x - self.map.viewport.x) * self.map.width / float(self.map.viewport.width)
        map_y = float(y - self.map.viewport.y) * self.map.height / float(self.map.viewport.height)
        return map_x, map_y

    def window_to_menu_coordinates(self, x, y):
        menu_x = float(x - self.sidemenu.viewport.x) * self.sidemenu.width / float(self.sidemenu.viewport.width) 
        menu_y = float(y - self.sidemenu.viewport.y) * self.map.height / float(self.sidemenu.viewport.height)
        return menu_x, menu_y

    def on_mouse_motion(self, x, y, dx, dy):
        if self.map.viewport.hit(x, y):
            x, y = self.window_to_map_coordinates(x, y)

            if self.current_mode == None and self.map.point_over_scrap(x,y):
                self.special_cursor = self.magnet_cursor
            elif self.current_mode == 'delete':
                if self.map.point_over_tower(x, y):
                    self.special_cursor = self.delete_cursor_active
                else:
                    self.special_cursor = self.delete_cursor_inactive             
            else:
                self.special_cursor = None

            # update coordinates of special cursor
            if self.special_cursor:
                self.special_cursor.x = x
                self.special_cursor.y = y

            # update coordinates of new tower icon
            if self.new_tower:
                self.new_tower.moved_yet = True
                self.new_tower.position = Vec2d(x,y)
                # determine if tower can be placed at this point
                self.new_tower.invalid_position = self.map.is_tower_invalid_at_point(x, y) or self.map.rect_overlaps_tower(self.new_tower.rectangle())
        else:
            self.special_cursor = None
        
    def on_mouse_press(self, x, y, button, modifiers):
        if self.sidemenu.viewport.hit(x, y):
            x, y = self.window_to_menu_coordinates(x, y)
            self.sidemenu.on_mouse_press(x, y, button, modifiers)
        
        elif self.map.viewport.hit(x, y):
            x, y = self.window_to_map_coordinates(x, y)
            if self.current_mode == 'new tower':
                # check if tower can be placed and if not, do nothing.
                if self.map.is_tower_invalid_at_point(x,y) or self.map.rect_overlaps_tower(self.new_tower.rectangle()):
                    return
                self.new_tower.position = Vec2d(x,y)
                self.map.add_object(self.new_tower)
                self.new_tower = None
                self.gp -= 125
                self.sidemenu.clear_buttons()
                self.current_mode = None
            elif self.current_mode == 'delete':
                tower = self.map.point_over_tower(x, y)
                if tower:
                    self.map.remove_object(tower)
                    tower_remove_sound.play()
                    self.delete_anim.reset()
                    self.delete_anim.x = tower.sprite.x
                    self.delete_anim.y = tower.sprite.y
                    self.temp_sprites.append(self.delete_anim)
            elif self.map.point_over_scrap(x,y):
                # Stop showing magnet cursor
                self.show_magnet = False
                # And replace it with the magnet cursor animation
                self.magnet_cursor_anim.reset()
                self.magnet_cursor_anim.x = x
                self.magnet_cursor_anim.y = y
                self.temp_sprites.append(self.magnet_cursor_anim)
                # Then play sound for magnet
                scrap_pickup_sound.play()
                # And pickup the robot scrap from map
                scrap = self.map.point_over_scrap(x,y)
                self.map.remove_object(scrap)
                # Then add money from scrap to bank
                self.gp += 100  
            else:
                # Look to see if mouse hit one of the towers, and if so
                # select it so that we can see its area of effect.
                tower = self.map.point_over_tower(x, y)
                if tower:
                    if tower == self.map.selected_tower:
                        self.map.selected_tower = None
                    else:
                        self.map.selected_tower = tower

    def on_key_press(self, symbol, modifiers):
        if symbol == pyglet.window.key.ESCAPE:
            pyglet.app.exit()
        elif symbol == pyglet.window.key._0 or symbol == pyglet.window.key.F:
            self.window.set_fullscreen(not self.window.fullscreen)
        elif symbol == pyglet.window.key._1:
            self.debug_fps = not self.debug_fps
        elif symbol == pyglet.window.key._2:
            self.debug_draw = not self.debug_draw
        elif symbol == pyglet.window.key.S:
            pyglet.image.get_buffer_manager().get_color_buffer().save('screenshot.png')
        else:
            # Pass keypress on to sidemenu to see if it corresponds 
            # to a button hotkey.
            self.sidemenu.on_key_press(symbol, modifiers)

    def create_tower_action(self, tower_class):
        """Returns an action method that can be attached to a tower button.
        Tower class is the type of tower that will be made when the button is pressed."""

        def button_action(flag):
            """Action method to enter "new tower" mode, meant to be
            attached to a button. If flag is false, unsets the mode."""
            if flag:
                self.current_mode = 'new tower'
                if not self.new_tower:
                    self.new_tower = tower_class(0,0)
                    self.new_tower.invalid_position = False
                    self.new_tower.moved_yet = False
            else:
                self.current_mode = None
                self.new_tower = None

        return button_action

    def delete_action(self, flag):
        """Button action method to enter delete mode."""
        if flag:
            self.current_mode = 'delete'
        else:
            self.current_mode = None

######################################################################

def run_game():
    window = MainWindow()
    window.clear()
    window.flip()
    window.set_visible(True)
    pyglet.app.run()

if __name__ == '__main__':
    run_game()
